#!/usr/bin/perl

package CSS::Preprocessor::Expression;

use strict;
use warnings;

use fields qw(position);

use constant SELECTOR_CONTEXT => 1;
use constant RULE_CONTEXT => 2;
use constant RULESET_CONTEXT => 3;
use constant VALUE_CONTEXT => 4;

use overload '""' => sub {$_[0]->as_string};

use Carp;

use CSS::Preprocessor::Expression::VariableReference;
use CSS::Preprocessor::Expression::If;
use CSS::Preprocessor::Expression::Foreach;
use CSS::Preprocessor::Expression::Var;
use CSS::Preprocessor::Expression::Literal;
use CSS::Preprocessor::Expression::Call;

use CSS::Preprocessor::ExpressionBlock;

use CSS::Preprocessor::Value::Null;
use CSS::Preprocessor::Value::Template;

sub parse {
    my ($class, $parser, $context) = @_;

    my $toker = $parser->tokenizer;

    my $next_token = $toker->peek_token();

    my $ret = undef;

    # Look for the long-hand at-keywords first
    if (UNIVERSAL::isa($next_token, 'CSS::Preprocessor::Token::AtKeyword')) {
        my $kw = $next_token->name;

        if (substr($kw, 0, 1) eq '-') {
            my $kw = substr($kw, 1);
            my $parse_method = "_parse_at_$kw";

            if (UNIVERSAL::can($class, $parse_method)) {
                $ret = $class->$parse_method($parser, $context);
            }
        }

    }
    elsif (UNIVERSAL::isa($next_token, 'CSS::Preprocessor::Token::VariableReference')) {
        my $varref = $toker->get_token();
        $ret = CSS::Preprocessor::Expression::VariableReference->new($varref->name);

        while ($toker->next_is_punctuation('(')) {
            my $expr = $ret;
            my @args = $class->_parse_parenthesized_expression_list($parser, $context);

            $ret = CSS::Preprocessor::Expression::Call->new(
                expr => $expr,
                args => \@args,
            );
        }
    }

    $parser->throw_error("Can't start an expression with ".$next_token, $next_token) unless $ret;
    $ret->set_position($next_token->position);
    return $ret;
}

sub new {
    my ($class) = @_;

    croak "$class is an abstract class" if $class eq __PACKAGE__;

    return fields::new($class);
}

sub set_position {
    my __PACKAGE__ $self = shift;
    my CSS::Preprocessor::BufferPos $pos = shift;

    $self->{position} = $pos;
}

sub position {
    return $_[0]->{position};
}

sub as_string {
    my $self = shift;

    my $class = ref($self);
    $class =~ s/^CSS::Preprocessor::Expression:://g;

    my $inside = $self->_as_string_inner;

    return $class.($inside ? "<$inside>" : '');
}

sub as_string_inner {
    return undef;
}

sub _parse_at_if {
    my ($class, $parser, $context) = @_;

    my $toker = $parser->tokenizer;

    $toker->require_at_keyword('-if');

    $toker->require_punctuation('(');
    my $if_cond = $class->parse($parser, $context);
    $toker->require_punctuation(')');

    my $if_block = CSS::Preprocessor::ExpressionBlock->parse($parser, $context);

    my @elseif_conds = ();
    my @elseif_blocks = ();

    while ($toker->next_is_at_keyword('-elseif')) {
        $toker->require_at_keyword('-elseif');
        $toker->require_punctuation('(');
        push @elseif_conds, $class->parse($parser, $context);
        $toker->require_punctuation(')');
        push @elseif_blocks, CSS::Preprocessor::ExpressionBlock->parse($parser, $context);
    }

    my $else_block;

    if ($toker->next_is_at_keyword('-else')) {
        $toker->require_at_keyword('-else');
        $else_block = CSS::Preprocessor::ExpressionBlock->parse($parser, $context);
    }

    return CSS::Preprocessor::Expression::If->new(
        if_cond => $if_cond,
        if_block => $if_block,
        elseif_conds => \@elseif_conds,
        elseif_blocks => \@elseif_blocks,
        else_block => $else_block,
    );

}

sub _parse_at_foreach {
    my ($class, $parser, $context) = @_;

    my $toker = $parser->tokenizer;

    $toker->require_at_keyword('-foreach');

    my @items = $class->_parse_parenthesized_expression_list($parser, VALUE_CONTEXT);
    my $block = CSS::Preprocessor::ExpressionBlock->parse($parser, $context);

    return CSS::Preprocessor::Expression::Foreach->new(
        items => \@items,
        block => $block,
    );

}

sub _parse_at_var {
    my ($class, $parser, $context) = @_;

    my $toker = $parser->tokenizer;

    $toker->require_at_keyword('-var');

    my $var_ref = $toker->get_token();

    unless (UNIVERSAL::isa($var_ref, 'CSS::Preprocessor::Token::VariableReference')) {
        die CSS::Preprocessor::ParseError->new_expecting_but_got(
            "a variable reference",
            $var_ref,
        );
    }

    $toker->require_punctuation('=');

    my $expr = $class->parse($parser, VALUE_CONTEXT);

    $parser->active_scope->set_var_expr($var_ref->name, $expr);

    return CSS::Preprocessor::Expression::Var->new($var_ref->name => $expr);
}

sub _parse_at_template {
    my ($class, $parser, $context) = @_;

    my $toker = $parser->tokenizer;

    $toker->require_at_keyword('-template');

    my $peek = $toker->peek_token();

    my $var_name = undef;

    if (UNIVERSAL::isa($peek, 'CSS::Preprocessor::Token::VariableReference')) {
        my $var_ref = $toker->get_token();
        $var_name = $var_ref->name;
    }

    $toker->require_punctuation('(');

    my @arg_names = ();

    while (1) {
        last if ($toker->next_is_punctuation(')'));

        my $var_ref = $toker->get_token();

        if (UNIVERSAL::isa($var_ref, 'CSS::Preprocessor::Token::VariableReference')) {
            push @arg_names, $var_ref->name;
        }
        else {
            die CSS::Preprocessor::ParseError->new_expecting_but_got(
                "a variable reference",
                $var_ref,
            );
        }

        last if ($toker->next_is_punctuation(')'));
        $toker->require_punctuation(',');
    }

    $toker->require_punctuation(')');

    # Create a scope for the arguments to live in at
    # evaluation time. This scope is initially empty.
    my $arg_scope = $parser->push_active_scope;

    my $block = CSS::Preprocessor::ExpressionBlock->parse($parser, $context);

    $parser->pop_active_scope($arg_scope);

    my $template = CSS::Preprocessor::Value::Template->new(
        arg_names => \@arg_names,
        arg_scope => $arg_scope,
        block => $block,
    );

    my $literal = CSS::Preprocessor::Expression::Literal->new($template);

    # If there was a variable name included earlier, then this
    # is syntactic sugar over a @-var construct with a template argument.
    if ($var_name) {
        $parser->active_scope->set_var_expr($var_name, $literal);
        return CSS::Preprocessor::Expression::Var->new($var_name => $literal);
    }
    else {
        return $literal;
    }
}

sub _parse_at_call {
    my ($class, $parser, $context) = @_;

    my $toker = $parser->tokenizer;
    $toker->require_at_keyword('-call');

    my $expr = $class->parse($parser, VALUE_CONTEXT);
    my @args = $class->_parse_parenthesized_expression_list($parser, VALUE_CONTEXT);

    return CSS::Preprocessor::Expression::Call->new(
        expr => $expr,
        args => \@args,
    );
}

sub _parse_at_null {
    my ($class, $parser, $context) = @_;

    $parser->tokenizer->require_at_keyword('-null');

    return CSS::Preprocessor::Expression::Literal->new(
        CSS::Preprocessor::Value::Null->fetch,
    );
}

sub _parse_parenthesized_expression_list {
    my ($class, $parser, $context) = @_;

    my $toker = $parser->tokenizer;

    my @items;

    $toker->require_punctuation('(');

    while (1) {
        last if ($toker->next_is_punctuation(')'));

        push @items, CSS::Preprocessor::Expression->parse($parser, $context);

        last if ($toker->next_is_punctuation(')'));
        $toker->require_punctuation(',');
    }

    $toker->require_punctuation(')');

    return @items;
}

=head1 NAME

CSS::Preprocessor::Expression - Abstract class representing expressions in CSS::Preprocessor

=cut

1;

